const entityMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;",
  "/": "&#x2F;",
  "`": "&#x60;",
  "=": "&#x3D;",
};

function escapeHtml(string) {
  return String(string).replace(/[&<>"'`=\/]/g, s => entityMap[s]);
}

function escapeRegExp(string) {
  return string.replace(/[-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}

function parseTemplate(template) {
  const tokens = [];
  let value;
  let temp = 0;
  let type;
  let token;
  let nonSpace = false;
  let hasTag = false;
  let spaces = [];
  let start;
  let tagIndex = 0;
  let lineHasNonSpace = false;
  let indendation = "";
  function stripSpace() {
    if (hasTag && !nonSpace) {
      while (spaces.length) {
        delete tokens[spaces.pop()];
      }
    } else {
      spaces = [];
    }
    nonSpace = false;
    hasTag = false;
  }
  let openingTagRe;
  let closingTagRe;
  let closingCurlyRe;
  function compileTags(tagsToCompile) {
    if (typeof tagsToCompile === "string") {
      tagsToCompile = tagsToCompile.split(/\s+/, 2);
    }
    if (!Array.isArray(tagsToCompile) || tagsToCompile.length !== 2) {
      throw new Error("不合法的标签：", tagsToCompile);
    }
    openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + "\\s*");
    closingTagRe = new RegExp("\\s*" + escapeRegExp(tagsToCompile[1]));
    closingCurlyRe = new RegExp("\\s*" + escapeRegExp("}" + tagsToCompile[1]));
  }
  compileTags("{{ }}");
  const scanner = new Scanner(template);
  while (!scanner.eos() && temp < 1000) {
    temp++;
    start = scanner.pos;
    // 找到{{之前的内容
    value = scanner.scanUntil(openingTagRe);
    if (value) {
      for (const chr of value) {
        if (/\s/.test(chr)) {
          spaces.push(tokens.length);
          indendation += chr;
        } else {
          nonSpace = true;
          indendation += " ";
          lineHasNonSpace = true;
        }
        tokens.push(["text", chr, start, ++start]);
        if (chr === "\n") {
          stripSpace();
          tagIndex = 0;
          lineHasNonSpace = false;
          indendation = "";
        }
      }
    }
    // 跳过{{
    if (!scanner.scan(openingTagRe)) {
      break;
    }
    hasTag = true;
    type = scanner.scan(/\{|#|\/|\^|!|\>|=/) || "name";
    scanner.scan(/\s*/);
    if (type === "=") {
      value = scanner.scanUntil(/\s*=/);
      scanner.scan(/\s*=/);
      scanner.scanUntil(closingTagRe);
    } else if (type === "{") {
      value = scanner.scanUntil(closingCurlyRe);
      scanner.scan(/\s*\}/);
      scanner.scanUntil(closingTagRe);
      type = "&";
    } else {
      value = scanner.scanUntil(closingTagRe);
    }

    if (!scanner.scan(closingTagRe)) {
      throw new Error("没有闭合标签");
    }

    if (type === ">") {
      token = [
        type,
        value,
        start,
        scanner.pos,
        indendation,
        tagIndex,
        lineHasNonSpace,
      ];
    } else {
      token = [type, value, start, scanner.pos];
    }
    tokens.push(token);
    tagIndex++;

    if (type === "name" || type === "{") {
      // 空格清除处理
      nonSpace = true;
    } else if (type === "=") {
      compileTags(value);
    }
  }
  stripSpace();

  return nestTokens(squashTokens(tokens));
}

function squashTokens(tokens) {
  const squashedTokens = [];
  let lastToken;
  for (const token of tokens) {
    if (token) {
      if (token[0] === "text" && lastToken && lastToken[0] === "text") {
        lastToken[1] += token[1];
        lastToken[3] = token[3];
      } else {
        lastToken = token;
        squashedTokens.push(token);
      }
    }
  }
  return squashedTokens;
}

function nestTokens(tokens) {
  const nestedTokens = [];
  let collector = nestedTokens;
  const sections = [];
  for (const token of tokens) {
    switch (token[0]) {
      case "#":
      case "^":
        collector.push(token);
        sections.push(token);
        collector = token[4] = [];
        break;
      case "/":
        const section = sections.pop();
        section[5] = token[2];
        collector = sections.length
          ? sections[sections.length - 1][4]
          : nestedTokens;
        break;
      default:
        collector.push(token);
    }
  }
  return nestedTokens;
}

class Context {
  constructor(view, parent) {
    this.view = view;
    this.parent = parent;
  }

  push(value) {
    return new Context(value, this);
  }

  lookup(name) {
    const context = this;
    let value = context.view[name];
    if (typeof value === "function") {
      value = value.call(this.view);
    }
    return value;
  }
}

class Scanner {
  constructor(template) {
    this.pos = 0;
    this.tail = template;
    this.string = template;
  }
  eos() {
    return this.tail === "";
  }
  scan(re) {
    const match = this.tail.match(re);
    // 没有匹配到或者没有在起始位置匹配到
    if (!match || match.index !== 0) {
      return "";
    }
    const string = match[0];
    this.tail = this.tail.substring(string.length);
    this.pos += string.length;
    return string;
  }
  scanUntil(re) {
    const index = this.tail.search(re);
    let match;
    if (index === -1) {
      match = this.tail;
      this.tail = "";
      this.pos = this.string.length;
    } else if (index === 0) {
      match = "";
    } else {
      match = this.tail.substring(0, index);
      this.tail = this.tail.substring(index);
      this.pos += index;
    }
    return match;
  }
}

class Writer {
  parse(template) {
    return parseTemplate(template);
  }
  render(template, view, partials) {
    const tokens = this.parse(template);
    console.log(tokens);
    const context = new Context(view);
    return this.renderTokens(tokens, context, partials, template);
  }
  renderTokens(tokens, context, partials, originalTemplate) {
    let buffer = "";
    let value;
    let symbol;
    for (const token of tokens) {
      value = "";
      symbol = token[0];
      if (symbol === "name") {
        value = this.escapedValue(token, context);
      } else if (symbol === "text") {
        value = token[1];
      } else if (symbol === "&") {
        value = this.unescapedValue(token, context);
      } else if (symbol === "#") {
        value = this.renderSecion(token, context, partials, originalTemplate);
      } else if (symbol === "^") {
        value = this.renderInverted(token, context, partials, originalTemplate);
      } else if (symbol === ">") {
        value = this.renderPartial(token, context, partials);
      }
      if (value != null) {
        buffer += value;
      }
    }
    return buffer;
  }
  escapedValue(token, context) {
    const value = context.lookup(token[1]);
    if (value != null) {
      return typeof value === "number" ? String(value) : escapeHtml(value);
    }
  }
  unescapedValue(token, context) {
    return context.lookup(token[1]);
  }
  renderSecion(token, context, partials, originalTemplate) {
    let value = context.lookup(token[1]);
    let buffer = "";
    const self = this;
    const subrender = function (template) {
      return self.render(template, context.view);
    };
    if (!value) {
      return;
    }
    if (Array.isArray(value)) {
      for (let item of value) {
        buffer += this.renderTokens(
          token[4],
          new Context(item),
          partials,
          originalTemplate
        );
      }
    } else if (typeof value === "function") {
      buffer = value.call(
        context.view,
        originalTemplate.substring(token[3], token[5]),
        subrender
      );
    } else if (typeof value === "object") {
      buffer = this.renderTokens(
        token[4],
        context.push(value),
        partials,
        originalTemplate
      );
    } else {
      buffer = this.renderTokens(token[4], context, partials, originalTemplate);
    }
    return buffer;
  }
  renderInverted(token, context, partials, originalTemplate) {
    const value = context.lookup(token[1]);
    if (!value || (Array.isArray(value) && value.length === 0)) {
      return this.renderTokens(token[4], context, partials, originalTemplate);
    }
  }
  renderPartial(token, context, partials) {
    const value = partials[token[1]];
    if (value) {
      let indendation = token[4];
      let tagIndex = token[5];
      let lineHasNonSpace = token[6];
      let indentedValue = value;
      console.log(lineHasNonSpace);
      if (tagIndex === 0) {
        indentedValue = this.indentPartial(value, indendation, lineHasNonSpace);
      }
      const tokens = this.parse(indentedValue);
      return this.renderTokens(tokens, context, partials, indentedValue);
    }
  }
  indentPartial(partial, indendation, lineHasNonSpace) {
    indendation = indendation.replace(/ \n/g, "");
    let partialByN1 = partial.split("\n");
    for (let i = 0; i < partialByN1.length; i++) {
      if (partialByN1[i] && (i > 0 || !lineHasNonSpace)) {
        partialByN1[i] = indendation + partial;
      }
    }
    return partialByN1.join("\n");
  }
}

const defaultWriter = new Writer();

const mustache = {
  escape: escapeHtml,
  render(template, view, partials) {
    document.getElementById("template").textContent = template;
    return defaultWriter.render(template, view, partials);
  },
};

export default mustache;
